1 module hunt.application.staticfile;
2 
3 import core.time;
4 import std.conv;
5 import std.string;
6 import std.datetime;
7 import std.path;
8 import std.digest.md;
9 import std.algorithm.searching : canFind;
10 static import std.stdio;
11 
12 import hunt;
13 import hunt.application.controller;
14 import hunt.application.config;
15 import hunt.utils.string;
16 import hunt.http.code;
17 
18 alias OnStaticFilePathSegmentation = string function(const string, const string) nothrow;
19 
20 class StaticfileController : Controller
21 {
22     mixin MakeController;
23 
24     __gshared OnStaticFilePathSegmentation onStaticFilePathSegmentation;
25     __gshared string staticFileMimetype = string.init;
26 
27     @Action
28     void doStaticFile()
29     {
30         if (request.route.staticFilePath == string.init)
31         {
32             response.do404();
33 
34             return;
35         }
36 
37         string staticFilename = mendPath(request.route.staticFilePath);
38 
39         if ((staticFilename == string.init) || (!std.file.exists(staticFilename)))
40         {
41             response.do404();
42 
43             return;
44         }
45 
46         FileInfo fi;
47         try
48         {
49             fi = makeFileInfo(staticFilename);
50         }
51         catch (Exception e)
52         {
53             response.doError(HTTPCodes.INTERNAL_SERVER_ERROR);
54 
55             return;
56         }
57 
58         if (fi.isDirectory)
59         {
60             response.do404();
61 
62             return;
63         }
64 
65         auto lastModified = toRFC822DateTimeString(fi.timeModified.toUTC());
66         auto etag = "\"" ~ hexDigest!MD5(staticFilename ~ ":" ~ lastModified ~ ":" ~ to!string(fi.size)).idup ~ "\"";
67 
68         response.setHeader(HTTPHeaderCode.LAST_MODIFIED, lastModified);
69         response.setHeader(HTTPHeaderCode.ETAG, etag);
70 
71         if (Config.app.application.staticFileCacheMinutes > 0)
72         {
73             auto expireTime = Clock.currTime(UTC()) + dur!"minutes"(Config.app.application.staticFileCacheMinutes);
74             response.setHeader(HTTPHeaderCode.EXPIRES, toRFC822DateTimeString(expireTime));
75             response.setHeader(HTTPHeaderCode.CACHE_CONTROL, "max-age=" ~ to!string(Config.app.application.staticFileCacheMinutes * 60));
76         }
77 
78         if ((request.headerExists(HTTPHeaderCode.IF_MODIFIED_SINCE) && (request.header(HTTPHeaderCode.IF_MODIFIED_SINCE) == lastModified)) ||
79             (request.headerExists(HTTPHeaderCode.IF_NONE_MATCH) && (request.header(HTTPHeaderCode.IF_NONE_MATCH) == etag)))
80         {
81             response.setHttpStatusCode(HTTPCodes.NOT_MODIFIED);
82 
83             return;
84         }
85 
86         auto mimetype = ((staticFileMimetype == string.init) ? getMimeContentTypeForFile(staticFilename) : staticFileMimetype);
87         response.setHeader(HTTPHeaderCode.CONTENT_TYPE, mimetype ~ ";charset=utf-8");
88 
89         response.setHeader(HTTPHeaderCode.ACCEPT_RANGES, "bytes");
90         ulong rangeStart = 0;
91         ulong rangeEnd = 0;
92 
93         if (request.headerExists(HTTPHeaderCode.RANGE))
94         {
95             // https://tools.ietf.org/html/rfc7233
96             // Range can be in form "-\d", "\d-" or "\d-\d"
97             auto range = request.header(HTTPHeaderCode.RANGE).chompPrefix("bytes=");
98             if (range.canFind(','))
99             {
100                 response.doError(HTTPCodes.NOT_IMPLEMENTED);
101 
102                 return;
103             }
104             auto s = range.split("-");
105 
106             if (s.length != 2)
107             {
108                 response.doError(HTTPCodes.BAD_REQUEST);
109 
110                 return;
111             }
112 
113             try
114             {
115                 if (s[0].length)
116                 {
117                     rangeStart = s[0].to!ulong;
118                     rangeEnd = s[1].length ? s[1].to!ulong : fi.size;
119                 }
120                 else if (s[1].length)
121                 {
122                     rangeEnd = fi.size;
123                     auto len = s[1].to!ulong;
124 
125                     if (len >= rangeEnd)
126                     {
127                         rangeStart = 0;
128                     }
129                     else
130                     {
131                         rangeStart = rangeEnd - len;
132                     }
133                 }
134                 else
135                 {
136                     response.doError(HTTPCodes.BAD_REQUEST);
137 
138                     return;
139                 }
140             }
141             catch (ConvException e)
142             {
143                 response.doError(HTTPCodes.BAD_REQUEST, e.msg);
144 
145                 return;
146             }
147 
148             if (rangeEnd > fi.size)
149             {
150                 rangeEnd = fi.size;
151             }
152 
153             if (rangeStart > rangeEnd)
154             {
155                 rangeStart = rangeEnd;
156             }
157 
158             if (rangeEnd)
159             {
160                 rangeEnd--; // End is inclusive, so one less than length
161             }
162             // potential integer overflow with rangeEnd - rangeStart == size_t.max is intended. This only happens with empty files, the + 1 will then put it back to 0
163 
164             response.setHeader(HTTPHeaderCode.CONTENT_LENGTH, to!string(rangeEnd - rangeStart + 1));
165             response.setHeader(HTTPHeaderCode.CONTENT_RANGE, "bytes %s-%s/%s".format(rangeStart < rangeEnd ? rangeStart : rangeEnd, rangeEnd, fi.size));
166             response.setHttpStatusCode(HTTPCodes.PARTIAL_CONTENT);
167         }
168         else
169         {
170             rangeEnd = fi.size - 1;
171             response.setHeader(HTTPHeaderCode.CONTENT_LENGTH, fi.size.to!string);
172         }
173 
174         // write out the file contents
175         auto f = std.stdio.File(staticFilename, "r");
176         scope(exit) f.close();
177 
178         f.seek(rangeStart);
179         auto buf = f.rawRead(new ubyte[rangeEnd.to!uint - rangeStart.to!uint + 1]);
180         response.setContext(buf);
181     }
182 
183 private:
184 
185     string mendPath(string path)
186     {
187         if (!path.startsWith(".") && !isAbsolute(path))
188         {
189             path = "./" ~ path;
190         }
191 
192         if (!path.endsWith("/"))
193         {
194             path ~= "/";
195         }
196 
197         string name = chompPrefix(request.path, request.route.getPattern());
198         
199         if (onStaticFilePathSegmentation !is null)
200         {
201             return onStaticFilePathSegmentation(path, name);
202         }
203 
204         return path ~ name;
205     }
206 
207     struct FileInfo {
208         string name;
209         ulong size;
210         SysTime timeModified;
211         SysTime timeCreated;
212         bool isSymlink;
213         bool isDirectory;
214     }
215 
216     FileInfo makeFileInfo(string fileName)
217     {
218         FileInfo fi;
219         fi.name = baseName(fileName);
220         auto ent = DirEntry(fileName);
221         fi.size = ent.size;
222         fi.timeModified = ent.timeLastModified;
223         version(Windows) fi.timeCreated = ent.timeCreated;
224         else fi.timeCreated = ent.timeLastModified;
225         fi.isSymlink = ent.isSymlink;
226         fi.isDirectory = ent.isDir;
227 
228         return fi;
229     }
230 
231     bool isCompressedFormat(string mimetype)
232     {
233         switch (mimetype)
234         {
235             case "application/gzip", "application/x-compress", "application/png", "application/zip",
236                     "audio/x-mpeg", "image/png", "image/jpeg",
237                     "video/mpeg", "video/quicktime", "video/x-msvideo",
238                     "application/font-woff", "application/x-font-woff", "font/woff":
239                 return true;
240             default: return false;
241         }
242     }
243 }